import string
import xapian
from django.db import models
from django.conf import settings
from datetime import datetime
from query import ResultSet, Hit
from base import Indexer

from djapian.backend.text import Text
from djapian import djapian_import

class DjapianStemmer:
    def __init__(self, django_model_instance, stemming_lang_accessor = "get_stemming_lang"):
        """ Construct a stemmer tailored for a particular model instance to index. """
        
        self.stemming_lang_accessor = stemming_lang_accessor
        
        language = None
        djapian_stemming_lang = getattr(settings, "DJAPIAN_STEMMING_LANG", None)
        
        # Language is not defined; no stemming will be performed
        if djapian_stemming_lang is None:
            language = "none"
        # A per-model language setting is used
        elif djapian_stemming_lang == "multi":
            language = self._get_lang_from_model(django_model_instance)
        # Use the language defined in DJAPIAN_STEMMING_LANG
        else:
            language = djapian_stemming_lang
        
        try:
            self.stemmer = xapian.Stem(language)
        except xapian.InvalidArgumentError, e:
            print "%s; disabling stemming for this document." % e
            self.stemmer = xapian.Stem("none")
    
    def stem_word(self, word):
        """ Return the stemmed form of a word """
        # This is the strangeness of the Xapian API which uses the () operator
        # to stem a word
        return self.stemmer(word)
    
    def stem_word_for_indexing(self, word):
        """ Return the stemmed form of a word with prefix for indexing """
        # We must prefix the stemmed form of the word with Z,
        # because the QueryParser add this prefix to each stemmed
        # words when it stems a search query.
        return "Z" + self.stem_word(word)
    
    def _get_lang_from_model(self, django_model_instance):
        """ Get the language from the Django model instance we are indexing.
            The language will be obtained if an accessor called by default
            'get_stemming_lang' is defined on the model. """
        
        # The name of the accessor is in self.stemming_lang_accessor
        if hasattr(django_model_instance, self.stemming_lang_accessor):
            accessor = getattr(django_model_instance, self.stemming_lang_accessor)
            if callable(accessor):
                return accessor()
            else:
                return accessor
        # No language defined
        return None

class XapianIndexer(Indexer):
    def update(self, documents=None):
        '''Update the database with the documents.
        There are some default value and terms in a document:
         * Values:
           1. Used to store the ID of the document
           2. Store the model of the object (in the string format, like "project.app.model")
           3..10. Free

         * Terms
           UID: Used to store the ID of the document, so we can replace the document by the ID
        '''
        # Open Xapian Database
        idx = xapian.WritableDatabase(self.path, xapian.DB_CREATE_OR_OPEN)

        # If doesnt have any document get all
        if documents is None:
            update_queue = self.model.objects.all()
        else:
            update_queue = documents

        # Get each document received
        for row in update_queue:
            doc = xapian.Document()
            #
            # Add default terms and values
            #
            doc.add_term("UID%d"%row.id)
            doc.add_value(1, '%d'%row.id)
            doc.add_value(2, '%s.%s'%(row.__class__.__module__, row.__class__.__name__))

            self.position = 1
            stemmer = DjapianStemmer(row, self.stemming_lang_accessor)
            self._process_text_fields(idx, row, doc, stemmer)
            self._process_attr_fields(row, doc, stemmer)
            
            idx.replace_document("UID%d"%(row.id), doc)
        idx.flush()
        del idx

    def _process_text_fields(self, idx, row, doc, stemmer):
        # Get each text field
        for field in self.text_fields:
            try:
                posting = ''
                # Get its value
                field_value = getattr(row,field.name)
                # A none value is not indexable
                if field_value is None:
                    continue
                # If it's a function, get it's result
                if callable(field_value):
                    field_value = field_value()
                # We consider the stripped value of the text
                if field_value.strip() != "":
                    posting = field_value
        
                # There's no content to add
                if not posting:
                    continue
        
                for field_v in Text().split(posting):
                    field_v = field_v.lower()
                    # A posting is an instance of a particular term indexing the document
                    # See http://www.xapian.org/docs/glossary.html
                    # Index both the term and it's stemmed form
                    for term in (field_v, stemmer.stem_word_for_indexing(field_v)):
                        doc.add_posting(
                            term, # Term
                            self.position, # Position
                            self.get_weight('.'.join((self.model._meta.object_name,field.name)), False) # Weight
                        )
                    idx.add_spelling(field_v)
                    self.position += 1
            except AttributeError, e:
                print 'AttributeError: %s'%e
            except UnicodeDecodeError, e:
                print 'UnicodeDecodeError: %s'%(e)

    def _process_attr_fields(self, row, doc, stemmer):
        valueno = 11 # This is the valueno used to sort docs, the firsts 10 values are reserved for internal use
        # Set all prefixed fields (as value and prefixed postings)
        for name, field in self.attr_fields.iteritems():
            try:
                # Get the field value based in the field name
                field_value = getattr(row, field.name)
                # A none value is not indexable
                if field_value is None:
                    continue
                # If it's a function get its content
                if callable(field_value):
                    field_value = field_value()
                
                # content_type is used to determine the type of the
                # data to index. This try/execpt is used to be able
                # to index data other than Django fields.
                content_type = field_value
                try:
                    content_type = row._meta.get_field(field.name)
                except:
                    pass
                
                if isinstance(content_type, models.IntegerField) \
                    or isinstance(content_type, int) or isinstance(content_type, long):
                    #
                    # Integer fields are stored with 12 leading zeros
                    #
                    doc.add_value(valueno, '%012d'%(field_value))
                elif isinstance(content_type, models.BooleanField) \
                    or isinstance(content_type, bool):
                    #
                    # Boolean fields are stored as 't' or 'f'
                    #
                    if field_value:
                        doc.add_value(valueno, 't')
                    else:
                        doc.add_value(valueno, 'f')
                elif isinstance(content_type, models.DateTimeField):
                    #
                    # DateTime fields are stored as %Y%m%d%H%M%S (better 
                    # sorting)
                    # 
                    doc.add_term('YEAR%d'%(field_value.year))
                    doc.add_term('MONTH%d'%(field_value.month))
                    doc.add_term('DAY%d'%(field_value.day))
                    doc.add_value(valueno, field_value.strftime('%Y%m%d%H%M%S'))
                else:
                    try:
                        doc.add_value(valueno, str(field_value))
                    except UnicodeEncodeError, e:
                        if isinstance(field_value, unicode):
                            doc.add_value(valueno, field_value.encode('utf-8'))
                        else:
                            doc.add_value(valueno, repr(field_value))

                valueno += 1
                if not isinstance(field_value, unicode):
                    field_value = unicode(str(field_value), 'utf-8')
                for field_v in Text().split(field_value):
                    try:
                        field_v = field_v.lower()
                        # Index both the term and it's stemmed form
                        for term in (field_v, stemmer.stem_word_for_indexing(field_v)):
                            doc.add_posting(
                                '%s%s'%(name.upper(), term), # Term
                                self.position, # Position
                                self.get_weight('.'.join((self.model._meta.object_name,name)), True) # Weight
                            )
                        self.position += 1
                    except UnicodeDecodeError, e:
                        print u'Forgoting word "%s"'%(field_value)
            except AttributeError, e:
                print 'AttributeError: %s'%e
            except UnicodeDecodeError, e:
                print 'UnicodeDecodeError: %s'%(e)
    
    def search(self, query, order_by='RELEVANCE', offset=0, limit=1000, \
                     flags=None, stemming_lang=None):
        """ flags are as defined in the Xapian API :
            http://www.xapian.org/docs/apidoc/html/classXapian_1_1QueryParser.html
            Combine multiple values with bitwise-or (|)."""
        idx = xapian.Database(self.path)
        for path in self.add_database:
            idx.add_database(xapian.Database(path))
        enquire = xapian.Enquire(idx)

        if order_by == 'RELEVANCE':
            enquire.set_sort_by_relevance()
        else:
            ascending = False
            if isinstance(order_by, basestring) and order_by.startswith('-'):
                ascending = True

            while order_by[0] in '+-':
                order_by = order_by[1:]

            valueno = 11
            for name, v in self.attr_fields.iteritems():
                if name == order_by:
                    break
                valueno += 1
            enquire.set_sort_by_value_then_relevance(valueno, ascending)

        enquire.set_query(self.parse_query(query, idx, flags, stemming_lang))
        mset = enquire.get_mset(offset, limit)
        results = []
        for match in mset:
            results.append({
                'score':match[xapian.MSET_PERCENT],
                'uid':match[xapian.MSET_DOCUMENT].get_value(1),
                'model':match[xapian.MSET_DOCUMENT].get_value(2)
            })
        self.mset = mset
        return XapianResultSet(results,self)

    def related(self, query, count = 10, flags=None, stemming_lang=None):
        ''' Returns the related tags'''

        # Open the database
        db = xapian.Database(self.path)
        enq = xapian.Enquire(db)
        # Making the search
        enq.set_query(self.parse_query(query, db, flags, stemming_lang))
        res = enq.get_mset(0, 10)
        rset = xapian.RSet()

        for x in res:
            rset.add_document(x[xapian.MSET_DID])

        # Get the tags
        rel = enq.get_eset(100, rset)

        related_tag = []

        # List of tags
        for  r in rel:
            related_tag.append(r[0])

        del_list = []
        # Making the negative list
        for p in related_tag:
            if p[0] in string.ascii_uppercase:
                del_list.append(p)

        # Removing the not permited tags from the list
        for x in del_list:
            related_tag.remove(x)

        return related_tag[:count]

    def delete(self, doc_id):
        """Delete a document from Xapian"""
        try:
            idx = xapian.WritableDatabase(self.path, xapian.DB_CREATE_OR_OPEN)
            idx.delete_document('UID%d'%doc_id)
            del idx
        except (IOError, RuntimeError, xapian.DocNotFoundError), e:
            pass

    def parse_query(self, term, db, flags=None, stemming_lang=None):
        """Parse Queries"""
        # Instance Xapian Query Parser
        query_parser = xapian.QueryParser()

        for name, field in self.attr_fields.iteritems():
            query_parser.add_prefix(name.lower(), name.upper())

        query_parser.set_database(db)
        query_parser.set_default_op(xapian.Query.OP_AND)
        
        # Stemming
        # See http://code.google.com/p/djapian/wiki/Stemming
        # The stemming_lang parameter has priority; if it is defined, it is used.
        # If not, the DJAPIAN_STEMMING_LANG variable from settings.py is used,
        # if it is defined, not None, and not defined as "multi" (i.e. if it is
        # defined as a language such as 'en' or 'french')
        if stemming_lang is None:
            if hasattr(settings, "DJAPIAN_STEMMING_LANG"):
                if settings.DJAPIAN_STEMMING_LANG is not None:
                    if settings.DJAPIAN_STEMMING_LANG != "multi":
                        stemming_lang = settings.DJAPIAN_STEMMING_LANG
        
        if stemming_lang is not None:
            query_parser.set_stemmer(xapian.Stem(stemming_lang))
            query_parser.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
        
        if flags is not None:
            parsed_query = query_parser.parse_query(term, flags)
        else:
            parsed_query = query_parser.parse_query(term)
        
        # This will only work if the flag FLAG_SPELLING_CORRECTION is set
        self.corrected_query_string = query_parser.get_corrected_query_string()
        
        return parsed_query
    
    def get_corrected_query_string(self):
        return self.corrected_query_string

class XapianResultSet(ResultSet):
    def __init__(self, hits, indexer):
        self._hits = hits
        self._indexer = indexer

    def __len__(self):
        return self._indexer.mset.get_matches_estimated()
    count = __len__

    def __iter__(self):
        for hit in self._hits:
            yield XapianHit(hit,self._indexer, djapian_import(hit['model']))

    def __getitem__(self,pos):
        '''Allow use index-based access'''
        return XapianHit(self._hits[pos],self._indexer, djapian_import(self._hits['model']))

    def __getslice__(self,start,end):
        '''Allows use slices to retrive the information
        WARNING: This returns a generator, not a "list"
        '''
        for hit in self._hits[start:end]:
            yield XapianHit(hit,self._indexer, djapian_import(hit['model']))

class XapianHit(Hit):
    def get_pk(self):
        return self.data['uid']

    def __getitem__(self, item):
        return self.data.__getitem__(item)

    def get_score(self):
        return self.data['score']

    score = property(get_score)
